/*
* Copyright (C) 2020 ~ 2024 chaigec All Rights Reserved.
*
* Author:     chaigec <chaigec@163.com>
* Maintainer: chaigec <chaigec@163.com>

 * Use of this source code is governed by MIT license that can be found in the
 * LICENSE file in the root of the source tree. All contributing project authors
 * may be found in the AUTHORS file in the root of the source tree.
*/

#include "MkHttpClient.h"
#include "MkHttpRequest.h"
#include "MkHttpResponse.h"
#include "MkWebSocketMessage.h"
#include "MkHttpUri.h"
#include "MkHttpUtil.h"
#include "MkEvent/MkTimer.h"
#include "MkEvent/MkEvent.h"
#include "MkUtil/MkUtil.h"
#include "MkUtil/MkBase64.h"
#include "MkUtil/MkSha1.h"
#include "MkUtil/MkLog.h"

#ifdef MkUt
#define WsPingInterval  0
#else
#define WsPingInterval  5000 //millsec
#endif

CMkHttpClient::CMkHttpClient(CMkEvent *pEvent, CMkTimer* pTimer, const MkEventRemovedCallback& HttpEventRemovedCb)
    : CMkTcpClient(pEvent, pTimer, HttpEventRemovedCb)
    , m_bWebSocket(FALSE)
    , m_WebsocketRecvCb(nullptr)
    , m_WebsocketConnectedCb(nullptr)
    , m_WebsocketCloseCb(nullptr)
    , m_HttpResponseCb(nullptr)
    , m_nLen(LenHalfM)
    , m_nOffset(0)
    , m_bSendPing(FALSE)
{
    m_pRecvBuf = (Uint8*)malloc(LenHalfM);
    //MkDebugLog("new http client:%p\n", this);
}

Uint32 CMkHttpClient::WebSocketSendPing()
{
    CMkWebSocketMessage Message;
    Message.SetMark(TRUE);
    Message.SetFrameType(CMkWebSocketMessage::PingFrame);
    MkString strHeader;
    Message.PacketHeader(0, strHeader);

    CMkTcpClient::Send((Uint8*)strHeader.c_str(), strHeader.size());
    return NoneError;
}

Uint32 CMkHttpClient::WebSocketSendPong()
{
    CMkWebSocketMessage Message;
    Message.SetMark(TRUE);
    Message.SetFrameType(CMkWebSocketMessage::PongFrame);
    MkString strHeader;
    Message.PacketHeader(0, strHeader);
    CMkTcpClient::Send((Uint8*)strHeader.c_str(), strHeader.size());
    return NoneError;
}

Uint32 CMkHttpClient::SendRequest(const MkHttpMethod& Method, const MkString& HttpUrl, const MkMapString& MapParam, const MkString& Body, const MkMapString& MapHeader, const MkHttpResponseCb& ResponseCb, Uint32 TimeMilSeconds /*= 5000*/)
{
    m_bFree = FALSE;
    MkString strHost;
    MkString strDomain;
    Uint16 Port;
    MkString RealPath;
    Uint32 ErrorCode = CMkHttpUtil::HttpUrlParse(HttpUrl, strDomain, strHost, Port, RealPath);
    MkCheckErrorLog(ErrorCode, "http url parse error\n");

    CMkHttpRequest Request;
    Request.AddHeader("Content-Length", std::to_string(Body.size()));
    Request.SetMethod(Method);
    CMkHttpUri Uri;
    Uri.SetUriPath(RealPath);
    if (!MapParam.empty()) {
        Uri.SetMapParam(MapParam);
    }
    Request.SetUri(Uri);
    MkString GmtTime;
    NowMkTime.GetGMTString(GmtTime);
    Request.AddHeader("Data", GmtTime);
    Request.AddHeader("User-Agent", MkUserAgent);
    Request.AddHeader("Host", strDomain + ":" + std::to_string(Port));
    Request.AddHeader("Connection", "keep-alive");
    Request.AddHeader("Accept", "*/*");
    MkMapString::const_iterator it = MapHeader.begin();
    for (; it != MapHeader.end(); it++) {
        Request.AddHeader(it->first, it->second);
    }
    MkString Content;
    Request.ToString(Content);
    m_HttpResponseCb = ResponseCb;
    GetTimer()->AddDelayer([this]() {
        if (GetEvent()) {
            GetEvent()->AddPipTask([this]() {
                if (m_HttpResponseCb) {
                    m_HttpResponseCb(MkHttpStateTimeout, "");
                    m_HttpResponseCb = nullptr;
                    m_bFree = TRUE;
                }
            });
        }
    }, TimeMilSeconds, m_RequestDelayer);
    if (GetEvent()) {
        GetEvent()->AddPipTask([this, strHost, Port, Content, Body]() {
            //use event thread to connect server and send
            Uint32 ErrorCode = ConnectServer(strHost, Port, [this, Content,Body]() {
                CMkTcpClient::Send((Uint8*)Content.c_str(), Content.size());
                if (!Body.empty()) {
                    CMkTcpClient::Send((Uint8*)Body.c_str(), Body.size());
                }
            });
            if (NoneError != ErrorCode && m_HttpResponseCb) {
                m_HttpResponseCb(MkHttpStateServiceUnavailable, "");
                m_HttpResponseCb = nullptr;
                m_bFree = TRUE;
                return;
            }
        }, TRUE);
    }
    return NoneError;
}

Uint32 CMkHttpClient::WebSocketConnect(const MkString& Path, const MkWebSocketConnectedCb& ConnectedCb, const MkWebsocketRecvCb& RecvCb, const MkWebSocketCloseCb& CloseCb)
{
    m_WebSocketAccessKey = CMkUtil::CreateRandomString(Len16, m_WebSocketAccessKey);
    MkString strBuf = m_WebSocketAccessKey + MkString(WsUuid);
    MkString Sha1Out;
    MkSha1Encode(strBuf, Sha1Out);
    MkBase64Encode(Sha1Out, m_WsAccept);

    MkString strHost;
    MkString strDomain;
    Uint16 Port;
    MkString RealPath;
    Uint32 ErrorCode = CMkHttpUtil::WebsocketUrlParse(Path, strDomain, strHost, Port, RealPath);
    MkCheckErrorLog(ErrorCode, "http url parse error\n");

    CMkHttpRequest Request;
    Request.SetMethod(MkHttpMethodGet);
    Request.SetUri(RealPath);
    Request.AddHeader("Connection", "Upgrade");
    Request.AddHeader("Pragma", "no-cache");
    Request.AddHeader("Cache-Control", "no-cache");
    Request.AddHeader("User-Agent", MkUserAgent);
    Request.AddHeader("Host", strDomain + ":" + std::to_string(Port));
    Request.AddHeader("Upgrade", "websocket");
    Request.AddHeader("Sec-WebSocket-Version", "13");
    Request.AddHeader("Accept-Encoding", "gzip, deflate");
    Request.AddHeader("Accept-Language", "zh-CN,zh;q=0.9");
    Request.AddHeader("Sec-WebSocket-Key", m_WebSocketAccessKey);
    Request.AddHeader("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits");
    if (!m_WebSocketProtocol.empty()) {
        Request.AddHeader("Sec-WebSocket-Protocol", m_WebSocketProtocol);
    }
    m_WebsocketRecvCb = RecvCb;
    m_WebsocketCloseCb = CloseCb;
    m_WebsocketConnectedCb = ConnectedCb;

    MkString Content;
    Request.ToString(Content);

    if (GetEvent()) {
        GetEvent()->AddPipTask([this, strHost, Port, Content]() {
            Uint32 ErrorCode = ConnectServer(strHost, Port, [this, Content]() {
                CMkTcpClient::Send((Uint8*)Content.c_str(), Content.size());
            });
            if (NoneError != ErrorCode) {
                return;
            }
        }, TRUE);
    }

    return NoneError;
}

Uint32 CMkHttpClient::SendWebSocketMessage(const MkString& Content)
{
    CMkWebSocketMessage Message;
    Message.SetMark(TRUE);

    MkString strHeader;
    Message.PacketHeader(Content.size(), strHeader);
    MkString strSendContent;
    Message.PacketContent(Content, strSendContent);

    CMkTcpClient::Send((Uint8*)strHeader.c_str(), strHeader.size());
    CMkTcpClient::Send((Uint8*)strSendContent.c_str(), strSendContent.size());
    return NoneError;
}

BOOL CMkHttpClient::IsFree()
{
    return m_bFree;
}

Uint32 CMkHttpClient::SetNotFree()
{
    m_bFree = FALSE;
    return NoneError;
}

Uint32 CMkHttpClient::OnRecv()
{
    Uint32 nRecvLen = m_nLen - m_nOffset;
    if (0 == nRecvLen) {
        MkErrorLog("buffer size:%d not parse success\n", m_nLen);
        Close();
        return UnknownError;
    }
    Uint32 ErrorCode = Recv(m_pRecvBuf + m_nOffset, nRecvLen);
    m_nOffset += nRecvLen;
    Uint32 nTotalUsed = 0;
    while (nTotalUsed < m_nOffset) {
        Uint32 nUsed = 0;
        if (m_bWebSocket) {
            ErrorCode = WebSocketRecvParse(m_pRecvBuf + nTotalUsed, m_nOffset - nTotalUsed, nUsed);
        } else {
            ErrorCode = HttpRecvParse(m_pRecvBuf + nTotalUsed, m_nOffset - nTotalUsed, nUsed);
        }
        if (NoneError != ErrorCode) {
            break;
        }
        nTotalUsed += nUsed;
    }
    if (m_nOffset != nTotalUsed) {
        memmove(m_pRecvBuf, m_pRecvBuf + nTotalUsed, m_nOffset - nTotalUsed);
    }
    m_nOffset -= nTotalUsed;
    return NoneError;
}

Uint32 CMkHttpClient::OnClose()
{
    if (m_bWebSocket
        && m_WebsocketCloseCb) {
        m_WebsocketCloseCb();
    }
    return CMkTcpClient::OnClose();
}

Uint32 CMkHttpClient::OnEventRemoved()
{
    GetTimer()->DeleteDelayer(m_RequestDelayer);
    return CMkTcpClient::OnEventRemoved();
}

Uint32 CMkHttpClient::ParseResponse(const CMkHttpResponse& pRes)
{
    //websocket response
    if (MkHttpStateSwitchProtocol == pRes.GetStatusCode()) {
        MkString Upgrade;
        MkString Connection;
        MkString WebSocketAccept;
        MkString WebSocketProtocol;
        pRes.GetHeadValue("Upgrade", Upgrade);
        pRes.GetHeadValue("Connection", Connection);
        pRes.GetHeadValue("Sec-WebSocket-Accept", WebSocketAccept);
        pRes.GetHeadValue("Sec-WebSocket-Protocol", WebSocketProtocol);
        if ("websocket" != Upgrade
            || "Upgrade" != Connection
            || m_WebSocketProtocol != WebSocketProtocol
            || WebSocketAccept.empty()
            || m_WsAccept != WebSocketAccept) {
            return InvalidParams;
        }
        m_bWebSocket = TRUE;
        if (m_WebsocketConnectedCb) {
            m_WebsocketConnectedCb();
        }
#ifdef MkUt
        WebSocketSendPing();
#endif
    } else {
        GetTimer()->DeleteDelayer(m_RequestDelayer);
        m_RequestDelayer.clear();
        if (m_HttpResponseCb) {
            //MkDebugLog("this:%p http client response:%d \n", this, pRes.GetStatusCode());
            m_HttpResponseCb(pRes.GetStatusCode(), pRes.GetBody());
            m_HttpResponseCb = nullptr;
            m_bFree = TRUE;
        }
    }
    return NoneError;
}

Uint32 CMkHttpClient::WebSocketRecvParse(const Uint8* pBuf, Uint32 nLen, Uint32& nUsed)
{
    CMkWebSocketMessage Message;
    Uint32 ErrorCode = Message.AddContent(pBuf, nLen, nUsed);
    MkCheckErrorLog(ErrorCode, "websocket message add content failed\n");
    if (Message.Mask()) {
        //MkErrorLog("response message can't be Mask\n");
        return InvalidParams;
    }
    switch (Message.Opcode()) {
    case CMkWebSocketMessage::PingFrame:
        WebSocketSendPong();
        break;
    case CMkWebSocketMessage::PongFrame:
        break;
    case CMkWebSocketMessage::CloseFrame:
    case CMkWebSocketMessage::ErrorFrame:
        if (m_WebsocketCloseCb) {
            m_WebsocketCloseCb();
        }
        Close();
        return SocketClosed;
    case CMkWebSocketMessage::BinaryFrame:
    case CMkWebSocketMessage::TextFrame:
        if (m_WebsocketRecvCb) {
            m_WebsocketRecvCb((const Uint8*)Message.GetPayloadData(), static_cast<Uint32>(Message.GetPayloadLength()));
        }
        m_WebsocketMsg.clear();
        break;
    case CMkWebSocketMessage::InCompleteFrame:
        m_WebsocketMsg += MkString(Message.GetPayloadData(), static_cast<Uint32>(Message.GetPayloadLength()));
        break;
    default:
        break;
    }
    return ErrorCode;
}

Uint32 CMkHttpClient::HttpRecvParse(const Uint8* pBuf, Uint32 nLen, Uint32& nUsed)
{
    Uint32 ErrorCode = m_Response.Parse(pBuf, nLen, nUsed);
    if (NotHasEnoughBuffer == ErrorCode) {
        return ErrorCode;
    } else if (NoneError != ErrorCode) {
        GetTimer()->DeleteDelayer(m_RequestDelayer);
        m_RequestDelayer.clear();
        if (m_HttpResponseCb) {
            //MkDebugLog("this:%p http client response 500 cb\n", this);
            m_HttpResponseCb(MkHttpStateInternalServerError, "");
            m_HttpResponseCb = nullptr;
            m_bFree = TRUE;
        }
        m_Response.Release();
        Close();
    } else {
        ErrorCode = ParseResponse(m_Response);
        m_Response.Release();
        if (m_Response.IsConnectClose()
            || NoneError != ErrorCode) {
            Close();
            return SocketClosed;
        }
    }
    return ErrorCode;
}

CMkHttpClient::~CMkHttpClient()
{
    m_bSendPing = FALSE;
    m_WebSocketAccessKey.clear();
    m_WebSocketProtocol.clear();
    m_WsAccept.clear();
    m_HttpResponseCb = nullptr;
    m_WebsocketRecvCb = nullptr;
    m_WebsocketCloseCb = nullptr;
    m_WebsocketConnectedCb = nullptr;
    GetTimer()->DeleteDelayer(m_RequestDelayer);
    m_RequestDelayer.clear();
    m_Response.Release();
    m_WebsocketMsg.clear();
    MkFree(m_pRecvBuf);
    //MkDebugLog("delete http client:%p\n", this);
}